﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using NLog;
using System.Threading;

namespace RaytracerLib.Cache
{
    public class CacheManager
    {
        #region CacheEntry
        private class CacheEntry
        {
            public object Data;
            public int MemoryUsage;
            public string FileName;
            public Type DataType;
            public DateTime ReleasedTime;
            public List<ICacheable> Users = new List<ICacheable>();

            public bool ContainsUser(ICacheable a_user)
            {
                for (int i = 0; i < Users.Count; i++)
                {
                    if (Object.ReferenceEquals(Users[i], a_user))
                        return true;
                }

                return false;
            }
        }
        #endregion

        public static long CACHE_SIZE = 1024 * 1024 * 1024;
        public static TimeSpan NOT_USED_PERIOD = new TimeSpan(0, 0, seconds: 10);
        public static int SLEEP_PERIOD_MS = 1000;

        private static long s_memory_usage;

        private static List<CacheEntry> s_cache = new List<CacheEntry>();
        private static List<CacheEntry> s_loading = new List<CacheEntry>();

        private static Logger Logger = LogManager.GetLogger("Cache");

        static CacheManager()
        {
            Task.Factory.StartNew(() => 
            {
                for (; ; )
                {
                    Thread.Sleep(SLEEP_PERIOD_MS);

                    lock (s_cache)
                    {
                        for (int i = s_cache.Count - 1; i >= 0; i--)
                        {
                            CacheEntry ce = s_cache[i];

                            if (ce.Users.Count != 0)
                                continue;

                            if (DateTime.Now - ce.ReleasedTime > NOT_USED_PERIOD)
                            {
                                s_cache.RemoveAt(i);
                                Interlocked.Add(ref s_memory_usage, -ce.MemoryUsage);

                                Logger.Info("Release not used data for: {0}", ce.FileName);

                                GC.Collect();
                            }
                        }
                    }
                }
            });
        }

        public static long MemoryUsage
        {
            get
            {
                return s_memory_usage;
            }
        }

        private static CacheEntry FindEntry(ICacheable a_user)
        {
            lock (s_cache)
            {
                foreach (var ce in s_cache)
                {
                    if (Object.ReferenceEquals(ce.Data, a_user.Data))
                    {
                        Debug.Assert(ce.Data != null);
                        Debug.Assert(ce.ContainsUser(a_user));

                        return ce;
                    }
                }

                return null;
            }
        }

        public static T GetOrLoad<T>(ICacheable a_user, Action a_loader) where T : class
        {
            CacheEntry nce = null;

            lock (s_cache)
            {
                foreach (var ce in s_cache)
                {
                    if (ce.FileName != a_user.FileName)
                        continue;

                    if (!(ce.Data is T))
                        continue;

                    nce = ce;
                    break;
                }

                if (nce == null)
                {
                    lock (s_loading)
                    {
                        foreach (var ce in s_loading)
                        {
                            if (ce.FileName != a_user.FileName)
                                continue;

                            if (!(ce.Data is T))
                                continue;

                            nce = ce;
                            break;
                        }


                        if (nce == null)
                        {
                            nce = new CacheEntry();

                            nce.DataType = typeof(T);
                            nce.FileName = a_user.FileName;

                            s_loading.Add(nce);
                        }
                    }
                }
            }

            lock (nce)
            {
                if (nce.Data != null)
                {
                    Logger.Info("Cached data finded: {0}", nce.FileName);
                    Debug.Assert(!nce.ContainsUser(a_user));
                    nce.Users.Add(a_user);

                    return (T)nce.Data;
                }
                else
                {
                    Logger.Info("Adding to cache: {0}", a_user.FileName);

                    a_loader();

                    nce.Data = a_user.Data;
                    nce.MemoryUsage = a_user.MemoryUsage;

                    Debug.Assert(!nce.ContainsUser(a_user));
                    nce.Users.Add(a_user);

                    Debug.Assert(nce.Data != null);
                    Debug.Assert(!String.IsNullOrWhiteSpace(nce.FileName));
                    Debug.Assert(nce.DataType == nce.Data.GetType());
                    Debug.Assert(nce.MemoryUsage > 0);

                    while (nce.MemoryUsage + MemoryUsage > CACHE_SIZE)
                    {
                        Logger.Info("Memory usage exceeded: {0} > {1}", nce.MemoryUsage + MemoryUsage, CACHE_SIZE);

                        if (!RemoveEntry())
                            throw new OutOfMemoryException("Increase cache size");

                        if (MemoryUsage == 0)
                            break;
                    }

                    Interlocked.Add(ref s_memory_usage, nce.MemoryUsage);

                    lock (s_cache)
                    {
                        s_cache.Add(nce);
                    }

                    lock (s_loading)
                    {
                        s_loading.Remove(nce);
                    }

                    return (T)nce.Data;
                }
            }
        }

        private static bool RemoveEntry()
        {
            CacheEntry ce = null;

            lock (s_cache)
            {
                DateTime last_usage_time = new DateTime(0);

                foreach (var c in s_cache)
                {
                    if (c.Users.Count != 0)
                        continue;

                    if (c.ReleasedTime >= last_usage_time)
                    {
                        last_usage_time = c.ReleasedTime;
                        ce = c;
                    }
                }
            
                if (ce == null)
                    return false;

                Logger.Info("Releasing not used cache data: {0}", ce.FileName);

                s_cache.Remove(ce);
                Interlocked.Add(ref s_memory_usage, -ce.MemoryUsage);
                return true;
            }
        }

        public static void Unbind(ICacheable a_user)
        {
            Logger.Info("Unbinding: {0}", a_user.FileName);

            lock (s_cache)
            {
                CacheEntry ce = FindEntry(a_user);

                Debug.Assert(ce != null);
                Debug.Assert(ce.ContainsUser(a_user));

                for (int i = 0; i < ce.Users.Count; i++)
                {
                    if (Object.ReferenceEquals(ce.Users[i], a_user))
                    {
                        ce.Users.RemoveAt(i);
                        break;
                    }
                }

                if (ce.Users.Count == 0)
                    ce.ReleasedTime = DateTime.Now;
            }
        }
    }
}